/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gr.staffpm.widget.jqgrid.component;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.Page;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.ResourceReference;
import org.apache.wicket.Response;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.internal.HeaderResponse;
import org.apache.wicket.markup.html.internal.HtmlHeaderContainer;
import org.apache.wicket.markup.parser.filter.HtmlHeaderSectionHandler;
import org.apache.wicket.markup.repeater.AbstractRepeater;
import org.apache.wicket.protocol.http.WebRequestCycle;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.request.target.component.IPageRequestTarget;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A request target that produces ajax response envelopes used on the client side to update
 * component markup as well as evaluate arbitrary javascript.
 * <p>
 * A component whose markup needs to be updated should be added to this target via
 * AjaxRequestTarget#addComponent(Component) method. Its body will be rendered and added to the
 * envelope when the target is processed, and refreshed on the client side when the ajax response is
 * received.
 * <p>
 * It is important that the component whose markup needs to be updated contains an id attribute in
 * the generated markup that is equal to the value retrieved from Component#getMarkupId(). This can
 * be accomplished by either setting the id attribute in the html template, or using an attribute
 * modifier that will add the attribute with value Component#getMarkupId() to the tag ( such as
 * MarkupIdSetter )
 * <p>
 * Any javascript that needs to be evaluated on the client side can be added using
 * AjaxRequestTarget#append/prependJavascript(String). For example, this feature can be useful when
 * it is desirable to link component update with some javascript effects.
 * <p>
 * The target provides a listener interface {@link IListener} that can be used to add code that
 * responds to various target events by adding listeners via
 * {@link #addListener(org.apache.wicket.ajax.AjaxRequestTarget.IListener)}
 * 
 * @author Graham Rhodes
 */
public class XMLDataRequestTarget implements IPageRequestTarget {

    /**
     * An {@link XMLDataRequestTarget} listener that can be used to respond to various target-related
     * events
     * 
     */
    public static interface IListener {

        /**
         * Triggered before ajax request target begins its response cycle
         * 
         * @param map
         *            read-only map:markupId->component of components already added to the target
         * @param target
         *            the target itself. Could be used to add components or to append/prepend
         *            javascript
         * 
         */
        public void onBeforeRespond(Map<String, Component> map, XMLDataRequestTarget target);

        /**
         * Triggered after ajax request target is done with its response cycle. At this point only
         * additional javascript can be output to the response using the provided
         * {@link IJavascriptResponse} object
         * 
         * NOTE: During this stage of processing any calls to target that manipulate the response
         * (adding components, javascript) will have no effect
         * 
         * @param map
         *            read-only map:markupId->component of components already added to the target
         * @param response
         *            response object that can be used to output javascript
         */
        public void onAfterRespond(Map<String, Component> map, IJavascriptResponse response);
    }

    /**
     * An ajax javascript response that allows users to add javascript to be executed on the client
     * side
     * 
     * @author ivaynberg
     */
    public static interface IJavascriptResponse {

        /**
         * Adds more javascript to the ajax response that will be executed on the client side
         * 
         * @param script
         *            javascript
         */
        public void addJavascript(String script);
    }

    /**
     * Response that uses an encoder to encode its contents
     * 
     * @author Igor Vaynberg (ivaynberg)
     */
    private final class AjaxResponse extends WebResponse {

        private final AppendingStringBuffer buffer = new AppendingStringBuffer(256);

        private boolean escaped = false;

        private final Response originalResponse;

        /**
         * Construct.
         * 
         * @param originalResponse
         */
        public AjaxResponse(Response originalResponse) {
            super(((WebResponse) originalResponse).getHttpServletResponse());
            this.originalResponse = originalResponse;
            setAjax(true);
        }

        /**
         * @see org.apache.wicket.Response#encodeURL(CharSequence)
         */
        @Override
        public CharSequence encodeURL(CharSequence url) {
            return originalResponse.encodeURL(url);
        }

        /**
         * @return contents of the response
         */
        public CharSequence getContents() {
            return buffer;
        }

        /**
         * NOTE: this method is not supported
         * 
         * @see org.apache.wicket.Response#getOutputStream()
         */
        @Override
        public OutputStream getOutputStream() {
            throw new UnsupportedOperationException("Cannot get output stream on StringResponse");
        }

        /**
         * @return true if any escaping has been performed, false otherwise
         */
        public boolean isContentsEncoded() {
            return escaped;
        }

        /**
         * Resets the response to a clean state so it can be reused to save on garbage.
         */
        @Override
        public void reset() {
            buffer.clear();
            escaped = false;

        }

        /**
         * @see org.apache.wicket.Response#write(CharSequence)
         */
        @Override
        public void write(CharSequence cs) {
            String string = cs.toString();
            if (needsEncoding(string)) {
                string = encode(string);
                escaped = true;
                buffer.append(string);
            } else {
                buffer.append(cs);
            }
        }

    }

    private static final Logger LOG = LoggerFactory.getLogger(XMLDataRequestTarget.class);

    private final List<String> appendJavascripts = new ArrayList<String>();

    private final List<String> domReadyJavascripts = new ArrayList<String>();

    /**
     * Create a response for component body and javascript that will escape output to make it safe
     * to use inside a CDATA block
     */
    private final AjaxResponse encodingBodyResponse;

    /**
     * Response for header contribution that will escape output to make it safe to use inside a
     * CDATA block
     */
    private final AjaxResponse encodingHeaderResponse;

    /** the component instances that will be rendered */
    private final Map<String, Component> markupIdToComponent = new LinkedHashMap<String, Component>();

    private final List<String> prependJavascripts = new ArrayList<String>();

    /** a list of listeners */
    private List<IListener> listeners = null;

    private final Page page;

    /**
     * 
     * @see org.apache.wicket.request.target.component.IPageRequestTarget#getPage()
     */
    @Override
    public Page getPage() {
        return page;
    }

    /**
     * Constructor
     * 
     * @param page
     */
    public XMLDataRequestTarget(Page page) {
        this.page = page;
        Response response = RequestCycle.get().getResponse();
        encodingBodyResponse = new AjaxResponse(response);
        encodingHeaderResponse = new AjaxResponse(response);
    }

    /**
     * Adds a listener to this target
     * 
     * @param listener
     */
    public void addListener(IListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Argument `listener` cannot be null");
        }

        if (listeners == null) {
            listeners = new LinkedList<IListener>();
        }

        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    /**
     * Visits all children of the specified parent container and adds them to the target if they are
     * of same type as <code>childCriteria</code>
     * 
     * @param parent
     * @param childCriteria
     */
    public final void addChildren(MarkupContainer parent, Class<?> childCriteria) {
        if (parent == null) {
            throw new IllegalArgumentException("Argument `parent` cannot be null");
        }
        if (childCriteria == null) {
            throw new IllegalArgumentException("Argument `childCriteria` cannot be null. If you want to traverse all components use `"
                    + Component.class.getName()
                    + ".class` as the value for this argument");
        }

        parent.visitChildren(childCriteria, new Component.IVisitor<Component>() {

            @Override
            public Object component(Component component) {
                addComponent(component);
                return CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
            }
        });
    }

    /**
     * Adds a component to the list of components to be rendered
     * 
     * @param component
     *            component to be rendered
     */
    public void addComponent(Component component) {
        if (component == null) {
            throw new IllegalArgumentException("component cannot be null");
        }
        if (component.getOutputMarkupId() == false) {
            throw new IllegalArgumentException("cannot update component that does not have setOutputMarkupId property set to true. Component: " + component.toString());
        }
        addComponent(component, component.getMarkupId());
    }

    /**
     * Adds a component to the list of components to be rendered
     * 
     * @param markupId
     *            id of client-side dom element that will be updated
     * 
     * @param component
     *            component to be rendered
     */
    public final void addComponent(Component component, String markupId) {
        if (Strings.isEmpty(markupId)) {
            throw new IllegalArgumentException("markupId cannot be empty");
        }
        if (component == null) {
            throw new IllegalArgumentException("component cannot be null");
        } else if (component instanceof Page) {
            if (component != page) {
                throw new IllegalArgumentException("component cannot be a page");
            }
        } else if (component instanceof AbstractRepeater) {
            throw new IllegalArgumentException(
                    "Component "
                            + component.getClass().getName()
                            + " has been added to the target. This component is a repeater and cannot be repainted via ajax directly. Instead add its parent or another markup container higher in the hierarchy.");
        }

        component.setMarkupId(markupId);
        markupIdToComponent.put(markupId, component);
    }

    /**
     * Returns an unmodifiable collection of all components added to this target
     * 
     * @return unmodifiable collection of all components added to this target
     */
    public final Collection<? extends Component> getComponents() {
        return Collections.unmodifiableCollection(markupIdToComponent.values());
    }

    /**
     * Adds javascript that will be evaluated on the client side after components are replaced
     * 
     * @deprecated use appendJavascript(String javascript) instead
     * @param javascript
     */
    @Deprecated
    public final void addJavascript(String javascript) {
        appendJavascript(javascript);
    }

    /**
     * Sets the focus in the browser to the given component. The markup id must be set. If the
     * component is null the focus will not be set to any component.
     * 
     * @param component
     *            The component to get the focus or null.
     */
    public final void focusComponent(Component component) {
        if (component != null && component.getOutputMarkupId() == false) {
            throw new IllegalArgumentException("cannot update component that does not have setOutputMarkupId property set to true. Component: " + component.toString());
        }
        final String id = component != null ? ("'" + component.getMarkupId() + "'") : "null";
        appendJavascript("Wicket.Focus.setFocusOnId(" + id + ");");
    }

    /**
     * Adds javascript that will be evaluated on the client side after components are replaced
     * 
     * @param javascript
     */
    public final void appendJavascript(String javascript) {
        if (javascript == null) {
            throw new IllegalArgumentException("javascript cannot be null");
        }

        appendJavascripts.add(javascript);
    }

    /**
     * @see org.apache.wicket.IRequestTarget#detach(org.apache.wicket.RequestCycle)
     */
    @Override
    public void detach(final RequestCycle requestCycle) {
        // detach the page if it was updated
        if (markupIdToComponent.size() > 0) {
            final Component component = markupIdToComponent.values().iterator().next();
            final Page page = component.findParent(Page.class);
            if (page != null) {
                page.detach();
            }
        }
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof XMLDataRequestTarget) {
            XMLDataRequestTarget that = (XMLDataRequestTarget) obj;
            return markupIdToComponent.equals(that.markupIdToComponent) && prependJavascripts.equals(that.prependJavascripts) && appendJavascripts.equals(that.appendJavascripts);
        }
        return false;
    }

    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        int result = "AjaxRequestTarget".hashCode();
        result += markupIdToComponent.hashCode() * 17;
        result += prependJavascripts.hashCode() * 17;
        result += appendJavascripts.hashCode() * 17;
        return result;
    }

    /**
     * Adds javascript that will be evaluated on the client side before components are replaced
     * 
     * @param javascript
     */
    public final void prependJavascript(String javascript) {
        if (javascript == null) {
            throw new IllegalArgumentException("javascript cannot be null");
        }

        prependJavascripts.add(javascript);
    }

    /**
     * Components can implement this interface to get a notification when AjaxRequestTarget begins
     * to respond. This can be used to postpone adding components to AjaxRequestTarget until the
     * response begins.
     * 
     * @author Matej Knopp
     */
    public static interface ITargetRespondListener {

        /**
         * Invoked when AjaxRequestTarget is about the respond.
         * 
         * @param target
         */
        public void onTargetRespond(XMLDataRequestTarget target);
    };

    private final Set<ITargetRespondListener> respondListeners = new HashSet<ITargetRespondListener>();

    /**
     * Register the given respond listener. The listener's
     * {@link ITargetRespondListener#onTargetRespond(XMLDataRequestTarget)} method will be invoked when
     * the {@link XMLDataRequestTarget} starts to respond.
     * 
     * @param listener
     */
    public void registerRespondListener(ITargetRespondListener listener) {
        respondListeners.add(listener);
    }

    @Override
    public void respond(RequestCycle requestCycle) {

    }


    /**
     * @see org.apache.wicket.IRequestTarget#respond(org.apache.wicket.RequestCycle)
     */
    public final void respond(final RequestCycle requestCycle, StringBuffer writer) {
        final WebResponse response = (WebResponse) requestCycle.getResponse();

        if (markupIdToComponent.values().contains(page)) {
            // the page itself has been added to the request target, we simply issue a redirect back
            // to the page
            final String url = requestCycle.urlFor(page).toString();
            response.redirect(url);
            return;
        }

        for (ITargetRespondListener listener : respondListeners) {
            listener.onTargetRespond(this);
        }

        final Application app = Application.get();

        // Determine encoding
        final String encoding = app.getRequestCycleSettings().getResponseRequestEncoding();

        // Set content type based on markup type for page
        response.setCharacterEncoding(encoding);
        response.setContentType("text/xml; charset=" + encoding);

        // Make sure it is not cached by a client
        response.setHeader("Expires", "Mon, 26 Jul 1997 05:00:00 GMT");
        response.setHeader("Cache-Control", "no-cache, must-revalidate");
        response.setHeader("Pragma", "no-cache");

        writer.append("<?xml version=\"1.0\" encoding=\"");
        writer.append(encoding);
        writer.append("\"?>");
        writer.append("<ajax-response>");

        // invoke onbeforerespond event on listeners
        fireOnBeforeRespondListeners();

        // normal behavior
        Iterator<String> it = prependJavascripts.iterator();
        while (it.hasNext()) {
            String js = it.next();
            respondInvocation(response, js, writer);
        }

        // process added components
        respondComponents(response, writer);

        fireOnAfterRespondListeners(response, writer);

        // execute the dom ready javascripts as first javascripts
        // after component replacement
        it = domReadyJavascripts.iterator();
        while (it.hasNext()) {
            String js = it.next();
            respondInvocation(response, js, writer);
        }
        it = appendJavascripts.iterator();
        while (it.hasNext()) {
            String js = it.next();
            respondInvocation(response, js, writer);
        }

        writer.append("</ajax-response>");
    }

    /**
     * 
     */
    private void fireOnBeforeRespondListeners() {
        if (listeners != null) {
            final Map<String, Component> components = Collections.unmodifiableMap(markupIdToComponent);

            Iterator<IListener> it = listeners.iterator();
            while (it.hasNext()) {
                (it.next()).onBeforeRespond(components, this);
            }
        }
    }

    /**
     * 
     * @param response
     */
    private void fireOnAfterRespondListeners(final WebResponse response, final StringBuffer writer) {
        // invoke onafterresponse event on listeners
        if (listeners != null) {
            final Map<String, Component> components = Collections.unmodifiableMap(markupIdToComponent);

            // create response that will be used by listeners to append
            // javascript
            final IJavascriptResponse jsresponse = new IJavascriptResponse() {

                @Override
                public void addJavascript(String script) {
                    respondInvocation(response, script, writer);
                }
            };

            Iterator<IListener> it = listeners.iterator();
            while (it.hasNext()) {
                (it.next()).onAfterRespond(components, jsresponse);
            }
        }
    }

    /**
     * Processes components added to the target. This involves attaching components, rendering
     * markup into a client side xml envelope, and detaching them
     * 
     * @param response
     */
    private void respondComponents(WebResponse response, StringBuffer writer) {

        // TODO: We might need to call prepareRender on all components upfront

        // process component markup
        Iterator<Map.Entry<String, Component>> it = markupIdToComponent.entrySet().iterator();
        while (it.hasNext()) {
            final Map.Entry<String, Component> entry = it.next();
            final Component component = entry.getValue();
            final String markupId = entry.getKey();

            if (!containsAncestorFor(component)) {
                respondComponent(response, markupId, component, writer);
            }
        }
    }

    /**
     * Checks if the target contains an ancestor for the given component
     * 
     * @param component
     * @return <code>true</code> if target contains an ancestor for the given component
     */
    private boolean containsAncestorFor(Component component) {
        Component cursor = component.getParent();
        while (cursor != null) {
            if (markupIdToComponent.containsValue(cursor)) {
                return true;
            }
            cursor = cursor.getParent();
        }
        return false;
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "[AjaxRequestTarget@"
                + hashCode()
                + " markupIdToComponent ["
                + markupIdToComponent
                + "], prependJavascript ["
                + prependJavascripts
                + "], appendJavascript ["
                + appendJavascripts
                + "]";
    }

    /**
     * Encodes a string so it is safe to use inside CDATA blocks
     * 
     * @param str
     * @return encoded string
     */
    protected String encode(String str) {
        if (str == null) {
            return null;
        }

        return Strings.replaceAll(str, "]", "]^").toString();
    }

    /**
     * @return name of encoding used to possibly encode the contents of the CDATA blocks
     */
    protected String getEncodingName() {
        return "wicket1";
    }

    /**
     * 
     * @param str
     * @return true if string needs to be encoded, false otherwise
     */
    protected boolean needsEncoding(String str) {
        /*
         * TODO Post 1.2: Ajax: we can improve this by keeping a buffer of at least 3 characters and
         * checking that buffer so that we can narrow down escaping occurring only for ']]>'
         * sequence, or at least for ]] if ] is the last char in this buffer.
         * 
         * but this improvement will only work if we write first and encode later instead of working
         * on fragments sent to write
         */

        return str.indexOf(']') >= 0;
    }

    /**
     * 
     * @param response
     * @param markupId
     *            id of client-side dom element
     * @param component
     *            component to render
     */
    private void respondComponent(final Response response, final String markupId, final Component component, StringBuffer writer) {
        if (component.getRenderBodyOnly() == true) {
            throw new IllegalStateException("Ajax render cannot be called on component that has setRenderBodyOnly enabled. Component: " + component.toString());
        }

        component.setOutputMarkupId(true);

        // substitute our encoding response for the real one so we can capture
        // component's markup in a manner safe for transport inside CDATA block
        final Response originalResponse = response;
        encodingBodyResponse.reset();
        RequestCycle.get().setResponse(encodingBodyResponse);

        // Initialize temporary variables
        final Page page = component.findParent(Page.class);
        if (page == null) {
            // dont throw an exception but just ignore this component, somehow
            // it got
            // removed from the page.
            // throw new IllegalStateException(
            // "Ajax request attempted on a component that is not associated
            // with a Page");
            LOG.debug("component: " + component + " with markupid: " + markupId + " not rendered because it was already removed from page");
            return;
        }

        page.startComponentRender(component);

        try {
            component.prepareForRender();

            // render any associated headers of the component
            respondHeaderContribution(response, component, writer);
        } catch (RuntimeException e) {
            try {
                component.afterRender();
            } catch (RuntimeException e2) {
                // ignore this one could be a result off.
            }
            // Restore original response
            RequestCycle.get().setResponse(originalResponse);
            encodingBodyResponse.reset();
            throw e;
        }

        try {
            component.renderComponent();
        } catch (RuntimeException e) {
            RequestCycle.get().setResponse(originalResponse);
            encodingBodyResponse.reset();
            throw e;
        }

        page.endComponentRender(component);

        // Restore original response
        RequestCycle.get().setResponse(originalResponse);

        writer.append("<component id=\"");
        writer.append(markupId);
        writer.append("\" ");
        if (encodingBodyResponse.isContentsEncoded()) {
            writer.append(" encoding=\"");
            writer.append(getEncodingName());
            writer.append("\" ");
        }
        writer.append("><![CDATA[");
        writer.append(encodingBodyResponse.getContents());
        writer.append("]]></component>");

        encodingBodyResponse.reset();
    }

    /**
     * Header response for an ajax request.
     * 
     * @author Matej Knopp
     */
    private class AjaxHeaderResponse extends HeaderResponse {

        private static final long serialVersionUID = 1L;

        private void checkHeaderRendering() {
            if (headerRendering == false) {
                throw new WicketRuntimeException("Only methods that can be called on IHeaderResponse outside renderHead() are renderOnLoadJavascript and renderOnDomReadyJavascript");
            }
        }

        @Override
        public void renderCSSReference(ResourceReference reference, String media) {
            checkHeaderRendering();
            super.renderCSSReference(reference, media);
        }

        @Override
        public void renderCSSReference(String url) {
            checkHeaderRendering();
            super.renderCSSReference(url);
        }

        @Override
        public void renderCSSReference(String url, String media) {
            checkHeaderRendering();
            super.renderCSSReference(url, media);
        }

        @Override
        public void renderJavascript(CharSequence javascript, String id) {
            checkHeaderRendering();
            super.renderJavascript(javascript, id);
        }

        @Override
        public void renderCSSReference(ResourceReference reference) {
            checkHeaderRendering();
            super.renderCSSReference(reference);
        }

        @Override
        public void renderJavascriptReference(ResourceReference reference) {
            checkHeaderRendering();
            super.renderJavascriptReference(reference);
        }

        @Override
        public void renderJavascriptReference(ResourceReference reference, String id) {
            checkHeaderRendering();
            super.renderJavascriptReference(reference, id);
        }

        @Override
        public void renderJavascriptReference(String url) {
            checkHeaderRendering();
            super.renderJavascriptReference(url);
        }

        @Override
        public void renderJavascriptReference(String url, String id) {
            checkHeaderRendering();
            super.renderJavascriptReference(url, id);
        }

        @Override
        public void renderString(CharSequence string) {
            checkHeaderRendering();
            super.renderString(string);
        }

        /**
         * Construct.
         */
        public AjaxHeaderResponse() {

        }

        /**
         * 
         * @see org.apache.wicket.markup.html.internal.HeaderResponse#renderOnDomReadyJavascript(java.lang.String)
         */
        @Override
        public void renderOnDomReadyJavascript(String javascript) {
            List<String> token = Arrays.asList(new String[] { "javascript-event", "window", "domready", javascript });
            if (wasRendered(token) == false) {
                domReadyJavascripts.add(javascript);
                markRendered(token);
            }
        }

        /**
         * 
         * @see org.apache.wicket.markup.html.internal.HeaderResponse#renderOnLoadJavascript(java.lang.String)
         */
        @Override
        public void renderOnLoadJavascript(String javascript) {
            List<String> token = Arrays.asList(new String[] { "javascript-event", "window", "load", javascript });
            if (wasRendered(token) == false) {
                // execute the javascript after all other scripts are executed
                appendJavascripts.add(javascript);
                markRendered(token);
            }
        }

        /**
         * 
         * @see org.apache.wicket.markup.html.internal.HeaderResponse#getRealResponse()
         */
        @Override
        protected Response getRealResponse() {
            return RequestCycle.get().getResponse();
        }
    };

    // whether a header contribution is being rendered
    private boolean headerRendering = false;
    private HtmlHeaderContainer header = null;

    private IHeaderResponse headerResponse;

    /**
     * Returns the header response associated with current AjaxRequestTarget.
     * 
     * Beware that only renderOnDomReadyJavascript and renderOnLoadJavascript can be called outside
     * the renderHeader(IHeaderResponse response) method. Calls to other render** methods will
     * result in an exception being thrown.
     * 
     * @return header response
     */
    public IHeaderResponse getHeaderResponse() {
        if (headerResponse == null) {
            headerResponse = new AjaxHeaderResponse();
        }
        return headerResponse;
    }

    /**
     * Header container component for ajax header contributions
     * 
     * @author Matej Knopp
     */
    private static class AjaxHtmlHeaderContainer extends HtmlHeaderContainer {

        private static final long serialVersionUID = 1L;

        /**
         * Construct.
         * 
         * @param id
         * @param target
         */
        public AjaxHtmlHeaderContainer(String id, XMLDataRequestTarget target) {
            super(id);
            this.target = target;
        }

        /**
         * 
         * @see org.apache.wicket.markup.html.internal.HtmlHeaderContainer#newHeaderResponse()
         */
        @Override
        protected IHeaderResponse newHeaderResponse() {
            return target.getHeaderResponse();
        }

        private final transient XMLDataRequestTarget target;
    };

    /**
     * 
     * @param response
     * @param component
     */
    private void respondHeaderContribution(final Response response, final Component component, StringBuffer writer) {
        headerRendering = true;

        // create the htmlheadercontainer if needed
        if (header == null) {
            header = new AjaxHtmlHeaderContainer(HtmlHeaderSectionHandler.HEADER_ID, this);
            final Page page = component.getPage();
            page.addOrReplace(header);
        }

        // save old response, set new
        Response oldResponse = RequestCycle.get().setResponse(encodingHeaderResponse);

        encodingHeaderResponse.reset();

        // render the head of component and all it's children

        component.renderHead(header);

        if (component instanceof MarkupContainer) {
            ((MarkupContainer) component).visitChildren(new Component.IVisitor<Component>() {

                @Override
                public Object component(Component component) {
                    if (component.isVisibleInHierarchy()) {
                        component.renderHead(header);
                        return CONTINUE_TRAVERSAL;
                    } else {
                        return CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
                    }
                }
            });
        }

        // revert to old response

        RequestCycle.get().setResponse(oldResponse);

        if (encodingHeaderResponse.getContents().length() != 0) {
            writer.append("<header-contribution");

            if (encodingHeaderResponse.isContentsEncoded()) {
                writer.append(" encoding=\"");
                writer.append(getEncodingName());
                writer.append("\" ");
            }

            // we need to write response as CDATA and parse it on client,
            // because
            // konqueror crashes when there is a <script> element
            writer.append("><![CDATA[<head xmlns:wicket=\"http://wicket.apache.org\">");

            writer.append(encodingHeaderResponse.getContents());

            writer.append("</head>]]>");

            writer.append("</header-contribution>");
        }

        headerRendering = false;
    }

    /**
     * 
     * @param response
     * @param js
     */
    private void respondInvocation(final Response response, final String js, StringBuffer writer) {
        boolean encoded = false;
        String javascript = js;

        // encode the response if needed
        if (needsEncoding(js)) {
            encoded = true;
            javascript = encode(js);
        }

        writer.append("<evaluate");
        if (encoded) {
            writer.append(" encoding=\"");
            writer.append(getEncodingName());
            writer.append("\"");
        }
        writer.append(">");
        writer.append("<![CDATA[");
        writer.append(javascript);
        writer.append("]]>");
        writer.append("</evaluate>");

        encodingBodyResponse.reset();
    }

    /**
     * Static method that returns current {@link XMLDataRequestTarget} or <code>null</code> of no
     * {@link XMLDataRequestTarget} is available.
     * 
     * @return {@link XMLDataRequestTarget} instance if current request is an Ajax request,
     *         <code>null</code> otherwise.
     */
    public static XMLDataRequestTarget get() {
        final RequestCycle requestCycle = RequestCycle.get();
        if (requestCycle != null) {
            if (requestCycle.getRequestTarget() instanceof XMLDataRequestTarget) {
                return (XMLDataRequestTarget) requestCycle.getRequestTarget();
            }
        }
        return null;
    }

    /**
     * Returns the HTML id of the last focused element.
     * 
     * @return markup id of last focused element, <code>null</code> if none
     */
    public String getLastFocusedElementId() {
        String id = ((WebRequestCycle) RequestCycle.get()).getWebRequest().getHttpServletRequest().getHeader("Wicket-FocusedElementId");
        return Strings.isEmpty(id) ? null : id;
    }
}
